﻿// This file is part of Code4Public (http://code4public.codeplex.com)
// Copyright 2011 Sina Iravanian - <sina@sinairv.com>
//
// This source file(s) may be redistributed, altered and customized
// by any means PROVIDING the authors name and all copyright
// notices remain intact.
// THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED. USE IT AT YOUR OWN RISK. THE AUTHOR ACCEPTS NO
// LIABILITY FOR ANY DATA DAMAGE/LOSS THAT THIS PRODUCT MAY CAUSE.
// ------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Code4Public.Languages;
using System.Diagnostics;

namespace Code4Public.Parsing
{
    public class ParserHelper
    {
        /// <summary>
        /// Reference to the language type, according to which, this parser class is going to parse input strings.
        /// </summary>
        private LanguageInfo languageInfo;

        public ParserHelper(LanguageInfo languageInfo)
        {
            Debug.Assert(languageInfo != null);
            this.languageInfo = languageInfo;
        }

        /// <summary>
        /// gets the type of the identifier (string of literals) and the group that it belong to (if any) as an out parameter.
        /// Different types possible are Keyword, and CustomWordGroup.
        /// </summary>
        /// <param name="identifier">the string of literals forming the identifier</param>
        /// <param name="group">the group that the identifier belongs to (if any, otherwise an empty string)</param>
        /// <returns></returns>
        private ExpressionType GetTypeOfIdentifier(string identifier, out string group)
        {
            group = "";

            if (languageInfo.ContainsKeyword(identifier))
                return ExpressionType.Keyword;

            foreach (CustomWordGroupInfo info in languageInfo.CustomWordGroups)
            {
                if (info.ContainsWord(identifier))
                {
                    group = info.ColorGroup;
                    return ExpressionType.WordGroup;
                }
            }

            return ExpressionType.Identifier;
        }

        /// <summary>
        /// Utility function to return a string of space characters with the given length
        /// </summary>
        private string GetSpaces(int len)
        {
            Debug.Assert(len > 0);

            var sb = new StringBuilder(len);
            for (int i = 0; i < len; ++i)
                sb.Append(" ");

            return sb.ToString();
        }

        /// <summary>
        /// Replaces the escape sequences with space characters in the order of their occurance.
        /// e.g. [\\"] contains both [\\] and [\"] but [\\] is replaced (with whitespaces) because it
        /// has occurred first.
        /// </summary>
        /// <param name="str">input string</param>
        /// <param name="escapes">array of escape sequences</param>
        private string ReplaceStringsInOrder(string str, string[] escapes)
        {
            int minFoundIndex = Int32.MaxValue;
            int curFoundIndex;
            string selectedEsc = "";

            do
            {
                minFoundIndex = Int32.MaxValue;
                foreach (string esc in escapes)
                {
                    curFoundIndex = str.IndexOf(esc);
                    if (0 <= curFoundIndex && curFoundIndex < minFoundIndex)
                    {
                        minFoundIndex = curFoundIndex;
                        selectedEsc = esc;
                    }
                }

                if (minFoundIndex < Int32.MaxValue) // i.e. sth was found
                {
                    string newStr = "";
                    if(minFoundIndex > 0)
                        newStr += str.Substring(0, minFoundIndex);
                    
                    newStr += GetSpaces(selectedEsc.Length);

                    if(minFoundIndex + selectedEsc.Length < str.Length)
                        newStr += str.Substring(minFoundIndex + selectedEsc.Length);

                    str = newStr;
                }

            } while (minFoundIndex < Int32.MaxValue);

            return str;
        }

        /// <summary>
        /// Checks whether an operator string contains and ending symbol for a delimiter or not.
        /// It recieves the expected ending symbol, and an array containing escape sequences to be ignored in the string.
        /// It then returns the substring right before the ending symbol, the ending symbol itself, and the substring right after the
        /// ending symbol as out parameters.
        /// </summary>
        /// <param name="strOp">String containing the operators</param>
        /// <param name="delimEscapes">array of escape sequences to be ignored in the input string</param>
        /// <param name="delimEndsWith">string of the ending symbol for the delimiter</param>
        /// <param name="preDelim">string right before the ending symbol</param>
        /// <param name="delimBoundary">the ending symbol itself</param>
        /// <param name="postDelim">string right after the ending symbol</param>
        private bool OperatorContainsDelimEnder(string strOp, string[] delimEscapes, string delimEndsWith,
            out string preDelim, out string delimBoundary, out string postDelim)
        {
            preDelim = delimBoundary = postDelim = "";

            string copy = strOp;
            copy = ReplaceStringsInOrder(copy, delimEscapes);

            int foundIndex = copy.IndexOf(delimEndsWith);

            if (foundIndex >= 0)
            {
                if (foundIndex > 0)
                    preDelim = strOp.Substring(0, foundIndex);
                else
                    preDelim = "";

                if (foundIndex + delimEndsWith.Length >= strOp.Length)
                    postDelim = "";
                else
                    postDelim = strOp.Substring(foundIndex + delimEndsWith.Length);

                delimBoundary = delimEndsWith;

                return true;
            }
            else
            {
                return false;
            }
        }
        
        /// <summary>
        /// Checks whether an operator string contains and starting symbol for a delimiter or not.
        /// It recieves the string of operators and then searches for all possible delimiter definitions to
        /// find one that matches the input string (if any).
        /// It then returns as out parameters, the string occurring right before the starting symbol, the starting symbol itself, 
        /// and the string occurring right after the starting symbol, the type of the matched delimiter, the group that the
        /// delimiter belongs to (if any, otherwise empty string), the ending symbol of the matched delimiter type,
        /// and an array of escape sequences defined for that special kind of delimiter.
        /// </summary>
        /// <param name="strOp">string of operators</param>
        /// <param name="preDelim">the string occurring right before the starting symbol</param>
        /// <param name="delimBoundary">the starting symbol itself</param>
        /// <param name="postDelim">the string occurring right after the starting symbol</param>
        /// <param name="delimType">the type of the matched delimiter</param>
        /// <param name="group">the group that the delimiter belongs to (if any, otherwise empty string)</param>
        /// <param name="endsWith">the ending symbol of the matched delimiter type</param>
        /// <param name="escapes">an array of escape sequences defined for that special kind of delimiter</param>
        private bool OperatorContainsDelimStarter(string strOp, out string preDelim, out string delimBoundary, out string postDelim,
            out ExpressionType delimType, out string group, out string endsWith, out string[] escapes)
        {
            Debug.Assert(strOp != null && strOp.Length > 0);

            preDelim = delimBoundary = postDelim = endsWith = group = "";
            escapes = null;
            delimType = ExpressionType.Newline;

            int minFoundIndex = Int32.MaxValue;
            int curFoundIndex;

            // searching StringInfos 
            foreach (var info in languageInfo.Strings)
            {
                curFoundIndex = strOp.IndexOf(info.StartsWith);
                if (curFoundIndex >= 0 && curFoundIndex < minFoundIndex)
                {
                    minFoundIndex = curFoundIndex;
                    delimBoundary = info.StartsWith;
                    delimType = ExpressionType.String;
                    group = "";
                    endsWith = info.EndsWith;
                    escapes = info.Escapes;
                }
            }

            // searching comment-lines
            foreach (var info in languageInfo.CommentLines)
            {
                curFoundIndex = strOp.IndexOf(info.StartsWith);
                if (curFoundIndex >= 0 && curFoundIndex < minFoundIndex)
                {
                    minFoundIndex = curFoundIndex;
                    delimBoundary = info.StartsWith;
                    delimType = ExpressionType.CommentLine;
                    group = "";
                    endsWith = "";
                    escapes = new string[0];
                }
            }

            // searching comments
            foreach (var info in languageInfo.Comments)
            {
                curFoundIndex = strOp.IndexOf(info.StartsWith);
                if (curFoundIndex >= 0 && curFoundIndex < minFoundIndex)
                {
                    minFoundIndex = curFoundIndex;
                    delimBoundary = info.StartsWith;
                    delimType = ExpressionType.Comment;
                    group = "";
                    endsWith = info.EndsWith;
                    escapes = new string[0];
                }
            }

            // searching custom delimited groups
            foreach (var info in languageInfo.CustomDelimitedInfos)
            {
                curFoundIndex = strOp.IndexOf(info.StartsWith);
                if (curFoundIndex >= 0 && curFoundIndex < minFoundIndex)
                {
                    minFoundIndex = curFoundIndex;
                    delimBoundary = info.StartsWith;
                    delimType = ExpressionType.DelimitedGroup;
                    group = info.ColorGroup;
                    endsWith = info.EndsWith;
                    escapes = new string[0];
                }
            }

            // --> End of searching for some delimited fields

            // having faond the minimum index for some delimited field
            if (0 <= minFoundIndex && minFoundIndex < strOp.Length)
            {
                // form preDelim (i.e. the content before delimiter boundary)
                if (minFoundIndex > 0) // if it has not happened right at the beginning of the string
                    preDelim = strOp.Substring(0, minFoundIndex);
                else 
                    preDelim = "";

                // form postDelim (i.e. the content right after the delimiter boundary)
                if (minFoundIndex + delimBoundary.Length >= strOp.Length) // if it's happened right at the end of the string
                    postDelim = "";
                else
                    postDelim = strOp.Substring(minFoundIndex + delimBoundary.Length);

                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Parses the string containing operators. Parsing operators is crucial because it may contain delimiters
        /// start and end characters, or may start and end several times in one single operator string.
        /// </summary>
        private Expression[] ParseOperator(string strOp, DelimiterInfo curDelimStatus, out DelimiterInfo nextDelimStatus)
        {
            // initializes out parameter with the worst-possible value
            nextDelimStatus = null;

            // list to hold expression found within the operator
            List<Expression> operators = new List<Expression>();

            // make a copy of curDelimStatus, so that changes made does not reflect in the caller's environment
            DelimiterInfo delimInfo = DelimiterInfo.Copy(curDelimStatus);
            
            // main loop variable
            bool finishedWithOperator = false;
            // will hold the delimited values, initialized with delimited values gathered so far in the caller environment
            StringBuilder sbDelimContent = new StringBuilder(delimInfo.DelimitedContentSoFar);
            // will hold the content to be processed in each iteration of the loop
            string expContent = "";
            // the type of the expression to be added to the list, initialized with the worst possible value
            ExpressionType curExp = ExpressionType.Newline;

            // --- temp variables to be used as out parameter
            // the type of the delimiter 
            ExpressionType delimType = ExpressionType.Newline; // some non-sence value to make it illigal at init time
            string delimEndsWith = "", delimGroup = "";
            string[] delimEscapes = null;
            string preDelim = "", delimBoundary = "", postDelim = "";
            // --- end of temp variables

            expContent = strOp;
            finishedWithOperator = false;
            while (!finishedWithOperator)
            {
                if (delimInfo.IsInsideDelim) // i.e. if some delimiter (e.g. comment, string, etc) has been started before
                {
                    if (!delimInfo.IsOneLineDelimiter) // i.e. if there are some endings criteria defined for this delim
                    {
                        if (OperatorContainsDelimEnder(expContent, delimInfo.DelimEscapes, delimInfo.DelimEndsWith,
                            out preDelim, out delimBoundary, out postDelim))
                        {
                            sbDelimContent.Append(preDelim);

                            // first add those prior to delim-boundary
                            if (sbDelimContent.ToString().Length > 0)
                            {
                                operators.Add(new Expression(sbDelimContent.ToString(), delimInfo.DelimType, delimInfo.DelimGroup));
                                sbDelimContent = new StringBuilder(); // empty delim content
                            }

                            // then add the boundary itself
                            operators.Add(new Expression(delimBoundary, delimInfo.DelimType, delimInfo.DelimGroup));

                            // next process stuff after the boundary
                            delimInfo.IsInsideDelim = false;

                            expContent = postDelim;
                            curExp = ExpressionType.Operator;
                        }
                    }

                    if (delimInfo.IsInsideDelim) // if all the 2 if-s above were not satisfied
                    {
                        sbDelimContent.Append(expContent);
                        if (sbDelimContent.ToString().Length > 0)
                        {
                            operators.Add(new Expression(sbDelimContent.ToString(), delimInfo.DelimType, delimInfo.DelimGroup));
                            sbDelimContent = new StringBuilder();
                        }

                        finishedWithOperator = true;
                    }
                }
                else // if !isInsideDelim
                {
                    if (OperatorContainsDelimStarter(expContent, out preDelim, out delimBoundary, out postDelim,
                        out delimType, out delimGroup, out delimEndsWith, out delimEscapes))
                    {
                        // fill the values of delimInfo
                        delimInfo.DelimType = delimType;
                        delimInfo.DelimGroup = delimGroup;
                        delimInfo.DelimEndsWith = delimEndsWith;
                        delimInfo.DelimEscapes = delimEscapes;
                        delimInfo.DelimStartsWith = delimBoundary;
                        delimInfo.IsInsideDelim = true;

                        // first add preDelim section as an operator
                        if (preDelim.Length > 0)
                            operators.Add(new Expression(preDelim, ExpressionType.Operator));

                        // then add the delim boundary as the delim itself 
                        operators.Add(new Expression(delimBoundary, delimInfo.DelimType, delimInfo.DelimGroup));

                        // finally process postDelim content, in comming iterations
                        expContent = postDelim;

                        // empty delimiter content holder
                        sbDelimContent = new StringBuilder();
                    }
                    else
                    {
                        if(expContent.Length > 0)
                            operators.Add(new Expression(expContent, ExpressionType.Operator));
                        finishedWithOperator = true;
                    }
                } // end of else [ of if (isInsideDelim) ]

                if (expContent.Length <= 0)
                    finishedWithOperator = true;

            } // end of while (!finishedWithOperator)

            // evaluate delimContents processed so far
            delimInfo.DelimitedContentSoFar = sbDelimContent.ToString();

            nextDelimStatus = delimInfo;

            return operators.ToArray();
        }

        /// <summary>
        /// Returns the sequence of expressions within the input string. 
        /// Expressions are the unit used for syntax highlighting.
        /// To know what are different kinds of Expressions refer to <code>ExpressionType</code> enum.
        /// </summary>
        public IEnumerable<Expression> GetExpressions(string input)
        {
            // will hold current status of delimiters in iterations
            DelimiterInfo curDelimStatus = DelimiterInfo.EmptyDelimInfo;

            // temp variable to be used as the out parameter
            DelimiterInfo nextDelimStatus = DelimiterInfo.EmptyDelimInfo;
            
            // reference to an array of expressions to be used for holding return value of process operators
            Expression[] opExpressions;

            // will hold delimited content during iterations
            StringBuilder sbDelimContent = new StringBuilder();

            // the group of the identifier, to be used as an out parameter
            string identifierGroup = "";

            // current expression type determined by the switch statement in the loop
            ExpressionType curExp = ExpressionType.Newline;

            // the content of the current expressions in each iteration
            string expContent = "";

            //--- the main loop
            foreach (Token token in GetTokens(input))
            {
                expContent = token.Content;
                switch (token.Type)
                {
                    case TokenType.Identifier:
                        curExp = GetTypeOfIdentifier(token.Content, out identifierGroup);
                        break;
                    case TokenType.Number:
                        curExp = ExpressionType.Number;
                        break;
                    case TokenType.Operator:
                        curDelimStatus.DelimitedContentSoFar = sbDelimContent.ToString();
                        opExpressions = ParseOperator(expContent, curDelimStatus, out nextDelimStatus);
                        
                        foreach (Expression ex in opExpressions)
                            yield return ex;

                        if (opExpressions.Length > 0)
                        {
                            curDelimStatus = nextDelimStatus;
                            sbDelimContent = new StringBuilder();
                            if (curDelimStatus.IsInsideDelim)
                                sbDelimContent.Append(curDelimStatus.DelimitedContentSoFar);
                        }

                        continue; // to skip stuff after switch
                    case TokenType.Whitespace:
                        curExp = ExpressionType.Whitespace;
                        break;
                    case TokenType.Newline:
                        if (curDelimStatus.IsInsideDelim)
                        {
                            // first return the remainder of the delimited content
                            yield return new Expression(sbDelimContent.ToString(), curDelimStatus.DelimType, curDelimStatus.DelimGroup);
                            // empty the delimited content
                            sbDelimContent = new StringBuilder();

                            // then return the new-line
                            yield return new Expression(Environment.NewLine, ExpressionType.Newline);

                            // in the end if the delimited group is not bound to one line turn isInsideDelim back on
                            curDelimStatus.IsInsideDelim = !curDelimStatus.IsOneLineDelimiter;
                            expContent = "";
                        }
                        else
                        {
                            curExp = ExpressionType.Newline;
                            expContent = Environment.NewLine;
                        }
                         
                        break;
                    default:
                        break;
                }

                if (curDelimStatus.IsInsideDelim)
                {
                    sbDelimContent.Append(expContent);
                }
                else
                {
                    if(expContent.Length > 0)
                        yield return new Expression(expContent, curExp, identifierGroup);
                }
            }

            if (sbDelimContent.Length > 0)
            {
                // if there is delimiter content left return the content
                yield return new Expression(sbDelimContent.ToString(), curDelimStatus.DelimType, curDelimStatus.DelimGroup);
            }

        }

        /// <summary>
        /// Returns the sequence of tokens within the input string. 
        /// To know what are different kinds of tokens refer to <code>TokenType</code> enum.
        /// </summary>
        protected IEnumerable<Token> GetTokens(string input)
        {
            Debug.Assert(input != null);

            int len = input.Length;
            if (len <= 0)
                yield break;
            
            StringBuilder sbCurrentToken = new StringBuilder();
            TokenType state = TokenType.Newline;
            TokenType newState = TokenType.Newline;

            // if the state == TokenType.Number then check this variable to see if the number was the hex number or not
            bool isHexNumber = false;

            char c; 
            bool tokenSwitch = false;
            for(int i = 0; i < len; ++i)
            {
                c = input[i];

                tokenSwitch = false;
                if (Char.IsDigit(c))
                {
                    if (state != TokenType.Number && state != TokenType.Identifier)
                    {
                        newState = TokenType.Number;
                        tokenSwitch = true;
                    }
                    else
                    {
                        newState = state;
                    }
                }
                else if (Char.IsLetter(c) || c == '_')
                {
                    // resolving x for hex numbers e.g. 0xAABDA23
                    if (state == TokenType.Number && (c == 'x' || c == 'X') 
                        && sbCurrentToken.ToString() == "0")
                    {
                        isHexNumber = true;
                        newState = TokenType.Number;
                    }
                    // resolving literal hex digits
                    else if (state == TokenType.Number && isHexNumber == true &&
                        (('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')))
                    {
                        newState = TokenType.Number;
                    }
                    // resolving e and E in scientific form of showing numbers
                    else if (state == TokenType.Number && (c == 'e' || c == 'E') && i < len - 1 &&
                        (Char.IsDigit(input[i + 1]) || input[i + 1] == '.' || input[i + 1] == '+' || input[i + 1] == '-'))
                    {
                        newState = TokenType.Number;
                    }
                    // consider first letter after number as the number itself e.g. 12.5d
                    else if (state == TokenType.Number && i > 0 && Char.IsDigit(input[i-1]) && !isHexNumber)
                    {
                        newState = TokenType.Number;
                    }
                    else
                    {
                        if (state != TokenType.Number && state != TokenType.Identifier)
                            tokenSwitch = true;

                        newState = TokenType.Identifier;
                    }
                }
                else if (c == '\r')
                {
                    if (i < len - 1 && input[i + 1] == '\n')
                    {
                        i++;
                        newState = TokenType.Newline;
                        tokenSwitch = true;
                    }
                }
                else if (c == '\n')
                {
                    newState = TokenType.Newline;
                    tokenSwitch = true;
                }
                else if (Char.IsWhiteSpace(c))
                {
                    newState = TokenType.Whitespace;
                    if (state != TokenType.Whitespace)
                        tokenSwitch = true;
                }
                else if (Char.IsControl(c))
                {
                    // ignore control chars
                }
                else if (Char.IsPunctuation(c) || Char.IsSymbol(c))
                {
                    // resolving decimal period
                    if ((state == TokenType.Whitespace || state == TokenType.Newline || state == TokenType.Number) && 
                        c == '.' && i < len - 1 && Char.IsDigit(input[i + 1]))
                    {
                        newState = TokenType.Number;
                    }
                    // resovling + and - in numbers
                    else if ((state == TokenType.Whitespace || state == TokenType.Newline || state == TokenType.Number) &&
                        (c == '-' || c == '+') && i < len - 1 && (Char.IsDigit(input[i + 1]) || input[i+1] == '.' ))
                    {
                        newState = TokenType.Number;
                    }
                    else
                    {
                        newState = TokenType.Operator;
                        if (state != TokenType.Operator)
                            tokenSwitch = true;
                    }
                }
                else
                {
                    // ignore remaining things
                }

                if (tokenSwitch)
                {
                    // reset state of isHexNumber
                    isHexNumber = false;

                    if (sbCurrentToken.ToString().Length > 0)
                    {
                        yield return new Token(sbCurrentToken.ToString(), state);
                        sbCurrentToken = new StringBuilder();
                    }
                }

                sbCurrentToken.Append(c);
                state = newState;
            }

            if(sbCurrentToken.ToString().Length > 0)
                yield return new Token(sbCurrentToken.ToString(), state);

        }

    }
}
